﻿/* 
* Copyright (c) 2010, The University of Texas at Austin
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, 
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, 
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, 
* this list of conditions and the following disclaimer in the documentation and/or other materials 
* provided with the distribution.
*
* Neither the name of The University of Texas at Austin nor the names of its contributors may be 
* used to endorse or promote products derived from this software without specific prior written 
* permission.
* 
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF 
* THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Media;
using System.Xml.Linq;
using Bio.Data;
using Bio.Data.Interfaces;
using Bio.Data.Providers;
using Bio.Data.Providers.Structure;
using Bio.Views.Structure.Editor.Internal.Data;
using LabelRNA;

namespace Bio.Views.Structure.Editor.Internal.Algorithms
{
    public class TemplateDiagram
    {
        public static StructureModelData CreateDiagramForSequenceByTemplate(StructureModelData templateModel, IBioSequence templateAlignedSequence, 
            IBioSequence targetAlignedSequence)
        {
            if (templateModel == null ||
                templateAlignedSequence == null ||
                targetAlignedSequence == null)
            {
                throw new ArgumentException("Template Diagram requires a valid Existing Model and Target Aligned Sequence");
            }

            SequenceRendering templateSequenceRendering = null;

            //1. We need the rendering map for the entity.
            if (templateModel.Entity != null && templateModel.Entity.Sequence != null)
            {
                IExtensibleProperties seqprops = templateModel.Entity.Sequence as IExtensibleProperties;
                if (seqprops.DoesExtendedPropertyExist(SequenceMetadata.Key))
                {
                    if (seqprops.GetExtendedProperty<SequenceMetadata>(SequenceMetadata.Key).Data.ContainsKey(SequenceRendering.Key))
                    {
                        templateSequenceRendering = (SequenceRendering)seqprops.GetExtendedProperty<SequenceMetadata>(SequenceMetadata.Key).Data[SequenceRendering.Key];
                    }
                    else { throw new ArgumentException("Template Diagram has no rendering"); }
                }
                else { throw new ArgumentException("Template Diagram has no rendering"); }
            }
            else { throw new ArgumentException("Template Diagram is missing or incomplete"); }

            //2. Intialize the model object for the target
            SequenceRendering targetSequenceRendering = new SequenceRendering();
            SimpleRNASequence targetModelSeq = new SimpleRNASequence();
            RNASecondaryStructureModel targetModel = new RNASecondaryStructureModel(null, targetModelSeq);



            StructureModelMetadata templateMetadata = (templateModel.Entity.StructureModelPropertyEdits != null) ? templateModel.Entity.StructureModelPropertyEdits :
                templateModel.Entity.StructureModelProperties;
            StructureModelMetadata targetMetadata = new StructureModelMetadata();

            if (templateMetadata != null)
            {
                if(templateMetadata.NucleotideRenderingMode.HasValue)
                    targetMetadata.NucleotideRenderingMode = templateMetadata.NucleotideRenderingMode.Value;

                if (templateMetadata.NucleotideCircleRendering != null)
                {
                    targetMetadata.NucleotideCircleRendering = new RNASecondaryStructureNucleotideCircleShape()
                    {
                        Diameter = templateMetadata.NucleotideCircleRendering.Diameter,
                        FillColor = templateMetadata.NucleotideCircleRendering.FillColor,
                        EdgeColor = templateMetadata.NucleotideCircleRendering.EdgeColor,
                        EdgeThickness = templateMetadata.NucleotideCircleRendering.EdgeThickness,
                        Filled = templateMetadata.NucleotideCircleRendering.Filled
                    };
                }

                if (templateMetadata.NucleotideTextRendering != null)
                {
                    targetMetadata.NucleotideTextRendering = new RNASecondaryStructureNucleotideTextShape()
                    {
                        Font = templateMetadata.NucleotideTextRendering.Font,
                        Color = templateMetadata.NucleotideTextRendering.Color,
                        FontStyle = templateMetadata.NucleotideTextRendering.FontStyle,
                        FontWeight = templateMetadata.NucleotideTextRendering.FontWeight,
                        FontSize = templateMetadata.NucleotideTextRendering.FontSize
                    };

                }

                if(templateMetadata.ConnectorStyle.HasValue)
                    targetMetadata.ConnectorStyle = templateMetadata.ConnectorStyle.Value;

                if(templateMetadata.ConnectorVisible.HasValue)
                    targetMetadata.ConnectorVisible = templateMetadata.ConnectorVisible.Value;

                if (templateMetadata.ConnectorRendering != null)
                {
                    targetMetadata.ConnectorRendering = new RNASecondaryStructureBasePairConnector()
                    {
                        FillColor = templateMetadata.ConnectorRendering.FillColor,
                        StrokeColor = templateMetadata.ConnectorRendering.StrokeColor,
                        Filled = templateMetadata.ConnectorRendering.Filled,
                        Size = templateMetadata.ConnectorRendering.Size,
                        EdgeThickness = templateMetadata.ConnectorRendering.EdgeThickness
                    };
                }

                targetModel.SetExtendedProperty(StructureModelMetadata.Key, targetMetadata);
            }

            targetModelSeq.SetExtendedProperty(SequenceMetadata.Key, new SequenceMetadata());
            targetModelSeq.GetExtendedProperty<SequenceMetadata>(SequenceMetadata.Key).Data.Add(SequenceRendering.Key, targetSequenceRendering);
            var targetSeqProjection = targetAlignedSequence.SequenceData.Where(elem => elem.Type == BioSymbolType.Nucleotide);
            foreach (var nt in targetSeqProjection)
            {
                targetModelSeq.AddSymbol(nt.Value);
            }
            targetModelSeq.Name = targetAlignedSequence.Name;
            targetModelSeq.Id = targetAlignedSequence.Id;
            targetModelSeq.SourceId = targetAlignedSequence.SourceId;
            targetModelSeq.ScientificName = targetAlignedSequence.ScientificName;
            targetModelSeq.TaxonomyId = targetAlignedSequence.TaxonomyId;

            //3. Project the template structure model onto the target sequence and create a rendering.
            Dictionary<int, int> equivNtByProjection = ProjectTemplateSequenceOnTargetSequence(templateSequenceRendering, targetSequenceRendering, templateAlignedSequence, targetAlignedSequence);

            //4. Set the pairs for the new structure model
            Dictionary<int, int> targetModelPairs = new Dictionary<int,int>();
            targetModel.BasePairs = new List<IBasePairBioEntity>();
            foreach (var templatePair in templateModel.Entity.BasePairs)
            {
                if (equivNtByProjection.ContainsKey(templatePair.FivePrimeIndex) && equivNtByProjection.ContainsKey(templatePair.ThreePrimeIndex))
                {
                    targetModel.BasePairs.Add(new RNASecondaryStructureBasePair(targetModelSeq, equivNtByProjection[templatePair.FivePrimeIndex], equivNtByProjection[templatePair.ThreePrimeIndex]));
                    targetModelPairs.Add(equivNtByProjection[templatePair.FivePrimeIndex], equivNtByProjection[templatePair.ThreePrimeIndex]);
                }
            }

            //5. Compute the extent information for the new structure model
            RNAParser aldenParser = new RNAParser(targetModelPairs, targetModelSeq.SequenceData.Count);
            XElement extentsXML = aldenParser.ParseToExtents();
            var extents = from extent in extentsXML.Descendants("Extent")
                          select new RNASecondaryStructureExtent(targetModelSeq,
                                                               Int32.Parse(extent.Element("ExtentID").Value),
                                                               Int32.Parse(extent.Element("ExtentOrdinal").Value),
                                                               (StructureExtentType)Enum.Parse(typeof(StructureExtentType), extent.Element("ExtentTypeID").Value, true),
                                                               Int32.Parse(extent.Element("ExtentStartIndex").Value),
                                                               Int32.Parse(extent.Element("ExtentEndIndex").Value));
            targetModel.Extents = extents.Cast<IStructureExtentBioEntity>().ToList();

            //6. Color the NT that we automatically positioned red
            IEnumerable<int> nonTemplatedNts = Enumerable.Range(0, targetModelSeq.SequenceData.Count).Except(equivNtByProjection.Values);
            foreach (var nonTemplateNt in nonTemplatedNts)
            {
                NucleotideMetadata metadata = new NucleotideMetadata();
                metadata.TextRendering = new RNASecondaryStructureNucleotideTextShape() { Color = Colors.Red.ToString(), FontWeight = FontWeights.Bold.ToString() };
                metadata.Visible = true;
                IExtensibleProperties props = targetModelSeq.SequenceData[nonTemplateNt] as IExtensibleProperties;
                if (props != null)
                {
                    props.SetExtendedProperty(NucleotideMetadata.Key, metadata);
                }
            }

            return new StructureModelData() { Entity = new EditableStructureModel(targetModel) };
        }

        private static int ConvertColumnIndexToSequenceIndex(int columnIndex, IBioSequence alignedSequence)
        {
            if (columnIndex >= 0 &&
                columnIndex < alignedSequence.SequenceData.Count &&
                columnIndex >= alignedSequence.FirstDataColumn)
            {
                IEnumerable<int> range = Enumerable.Range(alignedSequence.FirstDataColumn, (columnIndex - alignedSequence.FirstDataColumn));
                int nt = range.Where(idx => alignedSequence.SequenceData[idx].Type == BioSymbolType.Nucleotide).Count();
                return nt;
            }
            else { return -1; }
        }

        private static Dictionary<int, int> ProjectTemplateSequenceOnTargetSequence(SequenceRendering templateSequenceRendering, SequenceRendering targetSequenceRendering, IBioSequence templateAlignedSequence,
            IBioSequence targetAlignedSequence)
        {
            //Create a rendering for the target sequence based on the template diagram rendering and the alignment
            Dictionary<int, int> equivalentNtByAln = new Dictionary<int, int>();
            if (templateSequenceRendering != null)
            {
                int targetSequenceLength = targetAlignedSequence.SequenceData.Where(elem => elem.Type == BioSymbolType.Nucleotide).Count();
                for (int i = 0; i < targetSequenceLength; i++)
                    targetSequenceRendering.RenderingMap.Add(i, null);

                //1. Identify the equivalent columns between the two aligned sequences (this will hit the database).
                if (templateAlignedSequence.SequenceData.Count <= targetAlignedSequence.SequenceData.Count)
                {
                    var overlappingColumns = Enumerable.Range(0, templateAlignedSequence.SequenceData.Count).Where(columnIdx => (templateAlignedSequence.SequenceData[columnIdx].Type == BioSymbolType.Nucleotide && targetAlignedSequence.SequenceData[columnIdx].Type == BioSymbolType.Nucleotide));
                    foreach (var equivColumn in overlappingColumns)
                    {
                        int templateSeqIdx = ConvertColumnIndexToSequenceIndex(equivColumn, templateAlignedSequence);
                        int targetSeqIdx = ConvertColumnIndexToSequenceIndex(equivColumn, targetAlignedSequence);
                        equivalentNtByAln.Add(templateSeqIdx, targetSeqIdx);
                    }
                }
                else
                {
                    //Target sequence alignment is shorter than the template sequence alignment
                    var overlappingColumns = Enumerable.Range(0, targetAlignedSequence.SequenceData.Count).Where(columnIdx => (targetAlignedSequence.SequenceData[columnIdx].Type == BioSymbolType.Nucleotide && templateAlignedSequence.SequenceData[columnIdx].Type == BioSymbolType.Nucleotide));
                    foreach (var equivColumn in overlappingColumns)
                    {
                        int templateSeqIdx = ConvertColumnIndexToSequenceIndex(equivColumn, templateAlignedSequence);
                        int targetSeqIdx = ConvertColumnIndexToSequenceIndex(equivColumn, targetAlignedSequence);
                        equivalentNtByAln.Add(templateSeqIdx, targetSeqIdx);
                    }
                }

                //2. Sort the target sequence to template sequence mappings into consecutive regions of overlap that can be directly mapped.
                List<List<KeyValuePair<int, int>>> consecutiveGroups = new List<List<KeyValuePair<int, int>>>();

                var orderedByTemplateIdx = equivalentNtByAln.Keys.OrderBy(k => k);
                int prevTemplateIdx = -1;
                int prevTargetIdx = -1;
                List<KeyValuePair<int, int>> currentGroup = new List<KeyValuePair<int, int>>();
                foreach (var templateIdx in orderedByTemplateIdx)
                {
                    var targetIdx = equivalentNtByAln[templateIdx];
                    if (prevTemplateIdx == -1 && prevTargetIdx == -1)
                    {
                        //Were just starting, capture and continue
                        prevTemplateIdx = templateIdx;
                        prevTargetIdx = targetIdx;
                        continue;
                    }

                    if (prevTemplateIdx >= 0 && prevTargetIdx >= 0)
                    {
                        //We can make a consecutive comparison check
                        if ((targetIdx == prevTargetIdx + 1) && (templateIdx == prevTemplateIdx + 1))
                        {
                            currentGroup.Add(new KeyValuePair<int, int>(prevTemplateIdx, prevTargetIdx));
                            prevTemplateIdx = templateIdx;
                            prevTargetIdx = targetIdx;
                            //We continue the loop
                        }
                        else
                        {
                            //We break off a group
                            currentGroup.Add(new KeyValuePair<int, int>(prevTemplateIdx, prevTargetIdx));
                            consecutiveGroups.Add(currentGroup);
                            currentGroup = new List<KeyValuePair<int, int>>();
                            prevTemplateIdx = templateIdx;
                            prevTargetIdx = targetIdx;
                        }
                    }
                }

                //We have to check and see if the last iteration is part of a group or a new group.
                if (currentGroup.Count > 0)
                {
                    if (prevTemplateIdx == currentGroup.Last().Key + 1 && prevTargetIdx == currentGroup.Last().Value + 1)
                    {
                        currentGroup.Add(new KeyValuePair<int, int>(prevTemplateIdx, prevTargetIdx));
                        consecutiveGroups.Add(currentGroup);
                    }
                    else
                    {
                        consecutiveGroups.Add(currentGroup);
                        currentGroup = new List<KeyValuePair<int, int>>();
                        currentGroup.Add(new KeyValuePair<int, int>(prevTemplateIdx, prevTargetIdx));
                        consecutiveGroups.Add(currentGroup);
                    }
                }
                else
                {
                    currentGroup.Add(new KeyValuePair<int, int>(prevTemplateIdx, prevTargetIdx));
                    consecutiveGroups.Add(currentGroup);
                }

                //3. Position target sequence nt that are in consecutive groups of two or more members by straight mapping.
                var consecutiveGroupsFiltered = consecutiveGroups.Where(grp => (grp.Count >= 2));
                foreach (var cg in consecutiveGroupsFiltered)
                {
                    foreach (var pair in cg)
                    {
                        targetSequenceRendering.RenderingMap[pair.Value] = new LocalNucleotideSequenceVector(new Point(templateSequenceRendering.RenderingMap[pair.Key].StartX,
                            templateSequenceRendering.RenderingMap[pair.Key].StartY));
                    }
                        
                }

                //4. Now, we are going to position unmapped nucleotide near regions of consecutive nt.
                List<int> unmappedNtIdx = new List<int>();

                foreach (var key in targetSequenceRendering.RenderingMap.Keys.OrderBy(k => k))
                {
                    if (targetSequenceRendering.RenderingMap[key] == null)
                        unmappedNtIdx.Add(key);
                    else
                    {
                        //We hit a group.
                        PositionUnmappedNucleotides(key, unmappedNtIdx, targetSequenceRendering, templateSequenceRendering);
                        unmappedNtIdx.Clear();
                    }
                }

                //Target sequence is probably longer than the template sequence and we have some extra nucleotides to deal with at the end of the target sequence.
                if (unmappedNtIdx.Count > 0)
                {
                    int anchorPos = targetSequenceRendering.RenderingMap.Last(kvp => kvp.Value != null).Key;
                    PositionUnmappedNucleotides(anchorPos, unmappedNtIdx, targetSequenceRendering, templateSequenceRendering);
                    unmappedNtIdx.Clear();
                }

                //5. Re-calculate the vectors for the entire target sequence rendering based on the desired nt positioning.
                var allNt = targetSequenceRendering.RenderingMap.Keys.OrderBy(k => k);
                foreach (var nt in allNt)
                {
                    int nextNt = (nt + 1);
                    if (nextNt < targetSequenceRendering.RenderingMap.Keys.Count)
                    {
                        LocalNucleotideSequenceVector ntVector = new LocalNucleotideSequenceVector(new Point(targetSequenceRendering.RenderingMap[nt].StartX, targetSequenceRendering.RenderingMap[nt].StartY),
                            new Point(targetSequenceRendering.RenderingMap[nextNt].StartX, targetSequenceRendering.RenderingMap[nextNt].StartY));
                        targetSequenceRendering.RenderingMap[nt] = ntVector;
                    }
                }

                return equivalentNtByAln;
                
            }
            else { throw new ArgumentException("Template Diagram has no rendering"); }
        }

        private static void PositionUnmappedNucleotides(int firstNtInGroupIdx, List<int> unmappedNtIdx, SequenceRendering targetSequenceRendering, SequenceRendering templateSequenceRendering)
        {
            if (unmappedNtIdx != null && unmappedNtIdx.Count > 0)
            {
                //We will arrange the nt next to the first group member in a grid.
                if(firstNtInGroupIdx!=-1)
                    targetSequenceRendering.RenderingMap[unmappedNtIdx.First()] = new LocalNucleotideSequenceVector(new Point(targetSequenceRendering.RenderingMap[firstNtInGroupIdx].StartX + DEFAULT_NT_SPACING, targetSequenceRendering.RenderingMap[firstNtInGroupIdx].StartY));

                if (unmappedNtIdx.Count > 1)
                {
                    //Intermediate points.
                    int rowCount = 1;
                    double anchorXPos = targetSequenceRendering.RenderingMap[unmappedNtIdx.First()].StartX;
                    for (int i = 1; i < (unmappedNtIdx.Count - 1); i++)
                    {
                        if (rowCount % 5 == 0)
                        {
                            targetSequenceRendering.RenderingMap[unmappedNtIdx[i]] = new LocalNucleotideSequenceVector(new Point(anchorXPos, targetSequenceRendering.RenderingMap[unmappedNtIdx[i] - 1].StartY + DEFAULT_NT_SPACING));
                        }
                        else
                        {
                            targetSequenceRendering.RenderingMap[unmappedNtIdx[i]] = new LocalNucleotideSequenceVector(new Point(targetSequenceRendering.RenderingMap[unmappedNtIdx[i - 1]].StartX + DEFAULT_NT_SPACING, targetSequenceRendering.RenderingMap[unmappedNtIdx[i - 1]].StartY));
                        }

                        rowCount++;
                    }

                    //Now we will handle the last point, we'll put it to the right of the previous one
                    targetSequenceRendering.RenderingMap[unmappedNtIdx.Last()] = new LocalNucleotideSequenceVector(new Point(targetSequenceRendering.RenderingMap[unmappedNtIdx.Last() - 1].StartX + DEFAULT_NT_SPACING, targetSequenceRendering.RenderingMap[unmappedNtIdx.Last() - 1].StartY));
                }
            }
            unmappedNtIdx.Clear();
        }

        private static double DEFAULT_NT_SPACING = 8.0;
    }
}
